<template>
  <div ref="containerRef" class="virtual-list-container" v-on:scroll="onScroll">
    <div class="phantom-scroller" v-bind:style="phantomStyle">
      <div class="content-list" v-bind:style="contentStyle">
        <div
          class="list-item-wrapper"
          :class="{
            'is-highlighted': props.highlightedItemKey === item[props.itemKey],
          }"
          v-for="(item, index) in visibleData"
          v-bind:key="item[props.itemKey] || `item-${item.index}`"
          v-bind:data-index="realStartIndex + index"
        >
          <slot v-bind:item="item" v-bind:index="realStartIndex + index"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {
  ref,
  shallowRef,
  computed,
  onMounted,
  onUnmounted,
  watch,
  nextTick,
  type Ref,
  type ComputedRef,
  type ShallowRef,
} from "vue";
import type {
  VirtualListItemPosition,
  DataItem,
  DynamicVirtualListProps,
  ScrollToOptions,
  ScrollToNewItemOptions,
} from "./types";

const props = withDefaults(defineProps<DynamicVirtualListProps>(), {
  buffer: 5,
  itemKey: "id",
  resetScrollOnDataChange: false,
});

/** 容器 */
const containerRef: Ref<HTMLElement | null> = ref(null);
/** 重置观察器 */
const resizeObserver: Ref<ResizeObserver | null> = ref(null);
/** 视口高度 */
const viewportHeight: Ref<number> = ref(0);
/** 滚动条位置 */
const scrollTop: Ref<number> = ref(0);
/** 滚动定时器 */
const scrollTimer: Ref<number | null> = ref(null);
/** 可见数据 */
const visibleData: Ref<DataItem[]> = ref([]);
/** 内容区域偏移量 */
const contentOffset: Ref<number> = ref(0);
/** 真实起始索引 */
const realStartIndex: Ref<number> = ref(0);

/**
 * 位置缓存数组
 */
const positions: ShallowRef<VirtualListItemPosition[]> = shallowRef([]);

/**
 * @description 初始化所有列表项的位置缓存。
 * 基于预估高度计算每个元素的 top 和 bottom。
 */
const initPositions = (): void => {
  const newPositions: VirtualListItemPosition[] = [];
  let currentTop = 0;
  for (let i = 0; i < props.data.length; i++) {
    const newPos: VirtualListItemPosition = {
      index: i,
      height: props.estimatedHeight, // 初始为预估高度
      top: currentTop,
      bottom: currentTop + props.estimatedHeight,
    };
    newPositions.push(newPos);
    currentTop += props.estimatedHeight; // 累加顶部位置
  }
  positions.value = newPositions;
};

/**
 * @description 监听 props.data 的变化
 * 当数据源改变时，重新初始化位置
 */
watch(
  () => props.data,
  () => {
    const oldScrollTop = containerRef.value?.scrollTop || 0;

    initPositions(); // 重新计算位置

    // 只有在明确需要重置时才重置滚动位置
    if (props.resetScrollOnDataChange) {
      if (containerRef.value) {
        containerRef.value.scrollTop = 0; // 重置滚动条到顶部
      }
    } else {
      // 尝试保持滚动位置
      // 注意：如果数据长度变化很大，可能需要调整
      if (containerRef.value && oldScrollTop > 0) {
        // 等待 DOM 更新后再尝试恢复滚动位置
        nextTick(() => {
          if (containerRef.value) {
            const maxScrollTop = Math.max(
              0,
              phantomHeight.value - viewportHeight.value
            );
            containerRef.value.scrollTop = Math.min(oldScrollTop, maxScrollTop);
          }
        });
      }
    }

    updateVisibleData(); // 渲染新的可见数据
  },
  { deep: true } // 深度监听
);

/**
 * @description 组件挂载后执行
 * 获取视口高度，初始化位置缓存，并渲染首屏数据
 */
onMounted(() => {
  if (!containerRef.value) return;

  // 初始化视口高度
  viewportHeight.value = containerRef.value.clientHeight; // 获取容器（视口）的实际高度
  initPositions();
  updateVisibleData();

  // 监听容器 resize
  resizeObserver.value = new ResizeObserver((entries) => {
    for (let entry of entries) {
      if (entry.target === containerRef.value) {
        // 当容器高度变化时，更新 viewportHeight
        viewportHeight.value = containerRef.value.clientHeight;
        // 高度变化后，可能需要重新计算可见数据
        updateVisibleData();
      }
    }
  });
  resizeObserver.value.observe(containerRef.value);
});

/**
 * @description 组件卸载时，清除
 */
onUnmounted(() => {
  if (scrollTimer.value) {
    cancelAnimationFrame(scrollTimer.value);
  }
  // 停止监听 resize
  if (resizeObserver.value && containerRef.value) {
    resizeObserver.value.unobserve(containerRef.value);
  }
});

/**
 * @description 列表项总数
 */
const itemCount = computed(() => props.data.length);

/**
 * @description “幽灵”元素总高度
 * 它等于最后一个元素的位置缓存的 bottom 值
 */
const phantomHeight: ComputedRef<number> = computed(() => {
  if (itemCount.value === 0) return 0;
  // 总高度等于最后一项的 bottom
  return positions.value[positions.value.length - 1]?.bottom || 0;
});

/**
 * @description “幽灵”元素的内联样式
 */
const phantomStyle = computed(() => ({
  height: `${phantomHeight.value}px`,
}));

/**
 * @description 根据 scrollTop 使用二分查找获取起始索引
 * @param {number} st - 当前的 scrollTop 值
 * @returns {number} - 应该渲染的第一个元素的索引 (startIndex)
 */
const getStartIndex = (st: number): number => {
  let left = 0;
  let right = itemCount.value - 1;
  let tempIndex = -1; // 临时存储最接近的索引

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    // 安全检查，防止 positions[mid] 不存在
    if (!positions.value[mid]) {
      right = mid - 1;
      continue;
    }
    const midValue = positions.value[mid].top; // 获取中间元素的顶部位置

    if (midValue === st) {
      return mid; // 完美命中
    } else if (midValue < st) {
      left = mid + 1;
      // 当 midValue < st 时，mid 可能是我们要找的索引
      if (tempIndex === -1 || tempIndex < mid) {
        tempIndex = mid;
      }
    } else {
      right = mid - 1;
    }
  }
  // 返回最接近 scrollTop 的那个索引
  return tempIndex === -1 ? 0 : tempIndex;
};

/**
 * @description 根据 startIndex 获取结束索引
 * @param {number} startIndex - 起始索引
 * @returns {number} - 应该渲染的最后一个元素的索引 (endIndex)
 */
const getEndIndex = (startIndex: number): number => {
  let endIndex = startIndex;
  // 视口的底部位置 = 滚动条位置 + 视口高度
  const viewBottom = scrollTop.value + viewportHeight.value;

  // 循环直到找到第一个 top 值 > 视口底部的元素
  while (
    endIndex < itemCount.value &&
    positions.value[endIndex]?.top < viewBottom
  ) {
    endIndex++;
  }
  // 退出循环时，endIndex 是第一个 *不可见* 的元素，所以它作为 slice 的结束点是合适的
  return endIndex;
};

/**
 * @description 内容列表的内联样式
 * 使用 transform: translateY 来移动内容区域，性能最好
 */
const contentStyle = computed(() => ({
  transform: `translateY(${contentOffset.value}px)`,
}));

/**
 * @description 核心更新函数：计算并更新可见数据
 * 在滚动时被高频调用
 */
const updateVisibleData = (): void => {
  if (itemCount.value === 0 || !containerRef.value) {
    visibleData.value = []; // 如果没有数据或容器，清空可见数据
    return;
  }

  const st = scrollTop.value;

  // 获取起止索引
  const startIndex = getStartIndex(st);
  const endIndex = getEndIndex(startIndex);

  // 应用缓冲区
  const buffer = props.buffer;
  realStartIndex.value = Math.max(0, startIndex - buffer);
  const realEndIndex = Math.min(itemCount.value, endIndex + buffer);

  // 从总数据中切割出可见数据
  visibleData.value = props.data.slice(realStartIndex.value, realEndIndex);

  // 计算内容区域的偏移量 (offset)
  contentOffset.value = positions.value[realStartIndex.value]?.top || 0;
};

/**
 * @description 滚动事件处理器
 * 使用 requestAnimationFrame 进行节流
 * @param {Event} event - 滚动事件对象
 */
const onScroll = (event: Event): void => {
  if (!containerRef.value) return; // 安全检查

  if (scrollTimer.value) {
    cancelAnimationFrame(scrollTimer.value); // 取消上一个动画帧
  }

  // 使用 requestAnimationFrame 节流
  scrollTimer.value = requestAnimationFrame(() => {
    // 再次检查
    if (!containerRef.value) return;

    const target = event.target as HTMLElement;
    scrollTop.value = target.scrollTop;

    // 触发滚动回调
    if (props.onScroll) {
      props.onScroll(
        target.scrollTop,
        target.scrollHeight,
        target.clientHeight
      );
    }

    // 检查是否滚动到底部（容差 1px，避免浮点数精度问题）
    const isAtBottom =
      target.scrollTop + target.clientHeight >= target.scrollHeight - 1;
    if (isAtBottom && props.onScrollToBottom) {
      props.onScrollToBottom();
    }

    // 检查是否滚动到顶部
    if (target.scrollTop <= 1 && props.onScrollToTop) {
      props.onScrollToTop();
    }

    updateVisibleData(); // 在下一帧执行更新
  });
};

/**
 * @description (动态高度核心) 测量 DOM 节点的真实高度并更新缓存
 * 在 onUpdated 钩子中被调用
 */
const measureAndCache = (): void => {
  if (!containerRef.value || itemCount.value === 0) return;

  const nodes = containerRef.value.querySelectorAll(".list-item-wrapper");
  if (nodes.length === 0) return;

  let firstChangedIndex = -1; // 记录第一个发生高度变化的索引

  nodes.forEach((node) => {
    const indexStr = (node as HTMLElement).dataset.index;
    if (!indexStr) return;

    const index = parseInt(indexStr, 10);
    if (isNaN(index)) return;

    // 测量 DOM 的真实高度
    const rect = node.getBoundingClientRect();
    const realHeight = rect.height;
    const oldPos = positions.value[index];
    if (!oldPos) return; // 安全检查

    const oldHeight = oldPos.height;
    const diff = realHeight - oldHeight; // 计算真实高度与预估高度的差值

    if (diff !== 0) {
      // 如果高度有变化，更新缓存
      oldPos.height = realHeight;
      oldPos.bottom = oldPos.top + realHeight;

      // 只记录第一个变化的索引
      if (firstChangedIndex === -1) {
        firstChangedIndex = index;
      }
    }
  });

  // 如果有任何一项的高度发生了变化，
  // 我们必须更新它 *之后* 所有元素的位置
  if (firstChangedIndex !== -1) {
    updatePositionsAfter(firstChangedIndex);
  }
};

/**
 * @description (动态高度核心) 批量更新某个索引之后的所有元素位置
 * @param {number} changedIndex - 第一个发生高度变化的索引
 */
const updatePositionsAfter = (changedIndex: number): void => {
  // 我们从第一个变化项的 *下一个* 开始更新
  const startUpdateIndex = changedIndex + 1;

  if (startUpdateIndex >= itemCount.value) {
    return; // 如果变化的是最后一项，则无需更新
  }

  // 从该点开始，更新后续所有元素的 top 和 bottom
  for (let i = startUpdateIndex; i < itemCount.value; i++) {
    // 边界检查
    if (!positions.value[i] || !positions.value[i - 1]) continue;

    positions.value[i].top = positions.value[i - 1].bottom; // top 等于上一个的 bottom
    positions.value[i].bottom =
      positions.value[i].top + positions.value[i].height;
  }
  // phantomHeight 是一个 computed, 它会在 positions.value 变化后自动重新计算
};

/**
 * @description 监听 visibleData 变化
 * 替代 onUpdated，确保只在列表项变化时才测量
 */
watch(visibleData, async (newVisibleData, oldVisibleData) => {
  // 必须在 nextTick 中执行，确保 DOM 已经稳定
  await nextTick();
  measureAndCache();

  // 触发项可见性变化回调
  if (props.onItemVisible) {
    // 检查新可见的项
    newVisibleData.forEach((item, idx) => {
      const realIndex = realStartIndex.value + idx;
      const wasVisible = oldVisibleData?.some(
        (oldItem) => oldItem[props.itemKey] === item[props.itemKey]
      );
      if (!wasVisible) {
        props.onItemVisible?.(item, realIndex, true);
      }
    });

    // 检查不可见的项
    oldVisibleData?.forEach((oldItem) => {
      const isVisible = newVisibleData.some(
        (newItem) => newItem[props.itemKey] === oldItem[props.itemKey]
      );
      if (!isVisible) {
        const oldIndex = props.data.findIndex(
          (item) => item[props.itemKey] === oldItem[props.itemKey]
        );
        if (oldIndex !== -1) {
          props.onItemVisible?.(oldItem, oldIndex, false);
        }
      }
    });
  }
});

/**
 * @description 滚动到指定索引
 * @param {number | string} index - 要滚动到的目标索引
 * @param {ScrollToOptions} options - 滚动选项
 */
const scrollToIndex = (
  index: number | string,
  options?: ScrollToOptions
): void => {
  const numIndex = parseInt(String(index), 10);
  if (isNaN(numIndex)) return;

  // 保证索引在有效范围内
  const safeIndex = Math.max(0, Math.min(numIndex, itemCount.value - 1));

  if (containerRef.value && positions.value[safeIndex]) {
    const targetTop = positions.value[safeIndex].top + (options?.offset || 0);

    if (options?.behavior === "smooth") {
      containerRef.value.scrollTo({
        top: Math.max(0, targetTop),
        behavior: "smooth",
      });
    } else {
      // 直接设置 scrollTop 到该索引的 top
      // 这会自动触发 onScroll 事件, onScroll 会调用 updateVisibleData()
      containerRef.value.scrollTop = Math.max(0, targetTop);
    }
  }
};

/**
 * @description 滚动到底部
 */
const scrollToBottom = async (): Promise<void> => {
  if (!containerRef.value || itemCount.value === 0) return;

  // 先滚动到最后一项，确保底部项被渲染
  const lastIndex = itemCount.value - 1;
  scrollToIndex(lastIndex);

  // 等待渲染后，再精确滚动到底部
  await nextTick();

  const totalHeight = phantomHeight.value;
  if (containerRef.value) {
    containerRef.value.scrollTop = totalHeight - viewportHeight.value;
  }
};

/**
 * @description 通过 itemKey 滚动到指定项
 * @param {string | number} itemKey - 要滚动到的项的唯一 key
 * @param {ScrollToOptions} options - 滚动选项
 */
const scrollToItem = (
  itemKey: string | number,
  options?: ScrollToOptions
): void => {
  const index = props.data.findIndex((item) => item[props.itemKey] === itemKey);

  if (index === -1) {
    console.warn(`Item with key "${itemKey}" not found`);
    return;
  }

  scrollToIndex(index, options);
};

/**
 * @description 滚动到新添加的项
 * @param {string | number} itemKey - 新添加项的唯一 key
 * @param {ScrollToNewItemOptions} options - 滚动选项
 */
const scrollToNewItem = async (
  itemKey: string | number,
  options?: ScrollToNewItemOptions
): Promise<void> => {
  const index = props.data.findIndex((item) => item[props.itemKey] === itemKey);

  if (index === -1) {
    console.warn(`Item with key "${itemKey}" not found`);
    return;
  }

  // 先滚动到该项，确保被渲染
  scrollToIndex(index, {
    offset: options?.offset || 0,
    behavior: options?.behavior || "smooth",
  });

  // 等待渲染完成
  await nextTick();
  await nextTick(); // 可能需要两次 nextTick 确保 DOM 完全更新

  // 如果需要高亮，可以通过 emit 事件通知父组件
  // 这里我们通过 highlightedItemKey prop 来实现
  // 父组件需要监听这个 prop 的变化来添加高亮样式

  // 再次滚动确保位置正确（因为高度可能已经更新）
  if (containerRef.value && positions.value[index]) {
    const targetTop = positions.value[index].top + (options?.offset || 0);
    containerRef.value.scrollTo({
      top: Math.max(0, targetTop),
      behavior: options?.behavior || "smooth",
    });
  }
};

/**
 * @description 获取当前可见的项
 * @returns {DataItem[]} 当前可见的项数组
 */
const getVisibleItems = (): DataItem[] => {
  return visibleData.value;
};

/**
 * @description 获取当前可见项的 key 数组
 * @returns {(string | number)[]} 当前可见项的 key 数组
 */
const getVisibleItemKeys = (): (string | number)[] => {
  return visibleData.value.map((item) => item[props.itemKey]);
};

defineExpose({
  scrollToIndex,
  scrollToBottom,
  scrollToItem,
  scrollToNewItem,
  getVisibleItems,
  getVisibleItemKeys,
});
</script>

<style scoped>
.virtual-list-container {
  width: 100%;
  height: 100%;
  overflow-y: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
  will-change: transform;
  scroll-behavior: smooth;
  contain: layout style paint;
}

.phantom-scroller {
  position: relative;
  width: 100%;
}

.content-list {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  will-change: transform;
}

.list-item-wrapper {
  height: auto;
  contain: layout style;
}
</style>
